﻿using Sustainalytics.DataLayer;
using Sustainalytics.Entities;
using Sustainalytics.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using Ent = Sustainalytics.Entities;
using Newtonsoft.Json;
using Sustainalytics.Replication.Publisher;

namespace Sustainalytics.News.BusinessLogic
{
    /// <summary>
    /// News manager.
    /// </summary>
    public class NewsManagement : INewsManagement
    {
        /// <summary>
        /// News repository.
        /// </summary>
        private INewsRepository _newsRepo { get; set; }
        private IAccountsRepository _accountRepo { get; set; }
        private ServiceBusPublisher _serviceBusPublisher { get; set; }

        /// <summary>
        /// Ctor.
        /// </summary>
        /// <param name="newsRepo"> News repository. </param>
        public NewsManagement(INewsRepository newsRepo, IAccountsRepository accountsRepo, ServiceBusPublisher serviceBus)
        {
            this._newsRepo = newsRepo;
            this._accountRepo = accountsRepo;
            this._serviceBusPublisher = serviceBus;
        }
        
        /// <summary>
        /// Create news manager instance.
        /// </summary>
        /// <param name="newsRepo"> News repository. </param>
        /// <returns> News manager. </returns>
        public static INewsManagement GetInstance(INewsRepository newsRepo, IAccountsRepository accountsRepo, ServiceBusPublisher serviceBus)
        {
            INewsManagement newsMgr = new NewsManagement(newsRepo, accountsRepo, serviceBus);
            return newsMgr;
        }

        /// <summary>
        /// Get news paged.
        /// </summary>
        /// <param name="filter"> News filter. </param>
        /// <param name="pagingOptions"> Paging options. </param>
        /// <returns> Paged list of news. </returns>
        public List<Ent.News> GetNews(NewsFilter filter, Guid? accountId, int? skip, int? take, string[] orderBy = null, bool orderByAsc = true)
        {
            try
            {
                ValidateNewsFilter(filter);

                if (accountId != null)
                {
                    Account account = _accountRepo.GetAccountById(accountId.Value);
                    if (account.RelationshipType == RelationshipType.Client || account.RelationshipType == RelationshipType.Demo)
                        filter.AvailableFilter = NewsAvailability.Client;
                    else if (account.RelationshipType == RelationshipType.Prospect)
                        filter.AvailableFilter = NewsAvailability.Prospect;
                }

                List<Ent.News> newsLst = this._newsRepo.GetNews(filter, skip, take, orderBy, orderByAsc);


                return newsLst;
            }
            catch (Exception ex)
            {
                throw;
            }
        }

        public int GetNewsCount(NewsFilter filter)
        {
           
            try
            {
                ValidateNewsFilter(filter);
                int cnt = this._newsRepo.GetNewsCount(filter);
                return cnt;
            }
            catch (Exception ex)
            {
                throw;
            }
        }

        /// <summary>
        /// Get news by ID.
        /// </summary>
        /// <param name="newsId"> News ID. </param>
        /// <returns> News. </returns>
        public Ent.News GetNews(Guid newsId, Guid? accountId)
        {
            try
            {
                Ent.News news = this._newsRepo.GetNews(newsId);
                if (accountId != null)
                {
                    Account account = _accountRepo.GetAccountById(accountId.Value);

                    if ((account.RelationshipType == RelationshipType.Client || account.RelationshipType == RelationshipType.Demo) && 
                        (news.AvailableFor == NewsAvailability.Client || news.AvailableFor == NewsAvailability.All))
                        return news;
                    if (account.RelationshipType == RelationshipType.Prospect && (news.AvailableFor == NewsAvailability.Prospect || news.AvailableFor == NewsAvailability.All))
                        return news;
                    throw new NewsNotAvailableForAccount();
                }
                return news;
            }
            catch (Exception ex)
            {
                throw;
            }
        }

        /// <summary>
        /// Get multiple news by IDs.
        /// </summary>
        /// <param name="newsIds"> News IDs. </param>
        /// <returns> News list. </returns>
        public IEnumerable<Ent.News> GetNewsByIds(IEnumerable<Guid> newsIds)
        {
            string argsStr = LogArgument.GetString(newsIds);

            try
            {
                if (newsIds == null)
                {
                    throw new NewsIdsNullException();
                }

                IEnumerable<Ent.News> newsLst = this._newsRepo.GetNewsByIds(newsIds);

                return newsLst;
            }
            catch (Exception ex)
            {
                throw;
            }
        }

        /// <summary>
        /// Add news.
        /// </summary>
        /// <param name="news"> News data. </param>
        /// <returns> Added news. </returns>
        public Ent.News AddNews(Ent.News news)
        {
            string argsStr = LogArgument.GetString(news);

            try
            {
                // validate news
                ValidateNews(news);

                // set unpublish if current date passed unpublished date
                if (news.UnpublishedDate.HasValue && DateTime.Now.Date >= news.UnpublishedDate.Value.Date && news.Status != NewsStatus.Draft)
                {
                    news.Status = NewsStatus.Unpublished;
                }

                news.AddedByUserName = ClaimsPrincipal.Current.Claims.Where(s => s.Type == ClaimTypes.GivenName)
                    .Select(s => s.Value)
                    .SingleOrDefault() + " " + ClaimsPrincipal.Current.Claims.Where(s => s.Type == ClaimTypes.Surname)
                        .Select(s => s.Value)
                        .SingleOrDefault();
                news.AddedByUserId = new Guid(ClaimsPrincipal.Current.Claims.Where(c => c.Type == ClaimTypes.NameIdentifier).SingleOrDefault().Value);
                news.AddedByUserNameLoweCase = news.AddedByUserName.ToLowerInvariant();
 
                // add news
                this._newsRepo.AddNews(news);

                // get news
                Ent.News addedNews = this._newsRepo.GetNews(news.Id);

                if (addedNews == null)
                {
                    throw new NewsDoesNotExistException();
                }
                else
                {
                    //Serialize the newly added news an replicate to anoter region trough ServiceBus
                    _serviceBusPublisher.SendMessage(JsonConvert.SerializeObject(addedNews), ReplicationUtils.EntityType.News, ReplicationUtils.Operation.Create);                    
                }

                return addedNews;
            }
            catch (Exception ex)
            {
                throw;
            }
        }

        /// <summary>
        /// Update news.
        /// </summary>
        /// <param name="news"> News data. </param>
        /// <returns> Updated news. </returns>
        public Ent.News UpdateNews(Ent.News news)
        {
            string argsStr = LogArgument.GetString(news);

            try
            {
                // validate news data
                ValidateNews(news);

                // get news
                Ent.News existingNews = this._newsRepo.GetNews(news.Id);

                if (existingNews == null)
                {
                    throw new NewsDoesNotExistException();
                }

                // set unpublish if current date passed unpublished date
                if (news.UnpublishedDate.HasValue && DateTime.Now.Date >= news.UnpublishedDate.Value.Date && news.Status == NewsStatus.Published)
                {
                    news.Status = NewsStatus.Unpublished;
                }
                else
                {
                    // check whether news can be published/unpublished
                    if (news.Status != existingNews.Status
                        && news.Status == NewsStatus.Published
                        && existingNews.Status != NewsStatus.Draft)
                    {
                        throw new PublishNonDraftNewsException();
                    }
                }

                // update news
                this._newsRepo.UpdateNews(news);

                // get updated news
                Ent.News updatedNews = this._newsRepo.GetNews(news.Id);

                if (updatedNews == null)
                {
                    throw new NewsDoesNotExistException();
                }
                else
                {
                    //Serialize the updated news an replicate to anoter region trough ServiceBus
                    _serviceBusPublisher.SendMessage(JsonConvert.SerializeObject(updatedNews), ReplicationUtils.EntityType.News, ReplicationUtils.Operation.Update);
                }

                return updatedNews;
            }
            catch (Exception ex)
            {
                throw;
            }
        }

        public static void ValidateNews(Ent.News news)
        {
            List<NewsValidationException> validationExLst = new List<NewsValidationException>();

            if (news == null)
            {
                throw new NewsNullException();
            }

            // title
            if (string.IsNullOrEmpty(news.Title))
            {
                validationExLst.Add(new NewsValidationTitleNullOrEmptyException());
            }
            else
            {
                if (news.Title.Length > 200)
                {
                    validationExLst.Add(new NewsValidationTitleLengthException());
                }
            }

            // body
            if (string.IsNullOrEmpty(news.Body))
            {
                validationExLst.Add(new NewsValidationBodyNullOrEmptyException());
            }

            // region
            if (Enum.GetValues(typeof(NewsRegion)).Cast<int>().Any(val => val == (int)news.Region) == false)
            {
                validationExLst.Add(new NewsValidationRegionNotValidException());
            }

            // available for
            if (Enum.GetValues(typeof(NewsAvailability)).Cast<int>().Any(val => val == (int)news.AvailableFor) == false)
            {
                validationExLst.Add(new NewsValidationAvailableForNotValidException());
            }

            // overall
            if (validationExLst.Count > 0)
            {
                throw new NewsNotValidException(validationExLst.ToArray());
            }
        }

        public static void ValidateNewsFilter(NewsFilter filter)
        {
            if (filter == null)
            {
                return;
            }

            List<NewsFilterValidationException> errors = new List<NewsFilterValidationException>();

            if (filter.Category.HasValue && Enum.GetValues(typeof(NewsCategory)).Cast<int>().Any(val => val == (int)filter.Category) == false)
            {
                errors.Add(new NewsFilterValidationCategoryNotValidException());
            }

            if (errors.Count > 0)
            {
                throw new NewsFilterNotValidException(errors.ToArray());
            }
        }

        /// <summary>
        /// Delete news.
        /// </summary>
        /// <param name="newsId"> News ID to delete. </param>
        public void DeleteNews(Guid newsId)
        {
            try
            {
                // get news items
                Ent.News news = this._newsRepo.GetNews(newsId);

                if (news == null)
                    return;                
                else
                {
                    //Serialize the news id and replicate the delete to anoter region trough Service Bus
                    _serviceBusPublisher.SendMessage(JsonConvert.SerializeObject(newsId), ReplicationUtils.EntityType.News, ReplicationUtils.Operation.Delete);
                }
                
                // remove news items
                this._newsRepo.DeleteNews(newsId);

            }
            catch (Exception ex)
            {
                throw;
            }
        }

        /// <summary>
        /// Delete multiple news.
        /// </summary>
        /// <param name="newsItems"> News items to delete. </param>
        public void DeleteMultipleNews(IEnumerable<Guid> newsItems)
        {
            string argsStr = LogArgument.GetString(newsItems);

            try
            {
                if (newsItems == null)
                {
                    throw new NewsIdsNullException();
                }

                // get news items
                IEnumerable<Ent.News> newsLst = this._newsRepo.GetNewsByIds(newsItems);

                foreach (Ent.News news in newsLst)
                {
                    //Serialize the news id and replicate the delete to anoter region trough Service Bus
                    _serviceBusPublisher.SendMessage(JsonConvert.SerializeObject(news.Id), ReplicationUtils.EntityType.News, ReplicationUtils.Operation.Delete);
                }

                // remove news items
                this._newsRepo.DeleteMultipleNews(newsItems);

            }
            catch (Exception ex)
            {
                throw;
            }
        }

        /// <summary>
        /// Publish multiple news.
        /// </summary>
        /// <param name="newsIds"> News items to publish. </param>
        public void PublishMultipleNews(IEnumerable<Guid> newsIds)
        {
            string argsStr = LogArgument.GetString(newsIds);

            try
            {
                if (newsIds == null)
                {
                    throw new NewsIdsNullException();
                }

                // get news items
                IEnumerable<Ent.News> newsLst = this._newsRepo.GetNewsByIds(newsIds);

                // determine if all news are in Draft
                if (newsLst.Any(n => n.Status != NewsStatus.Draft))
                {
                    throw new PublishNonDraftNewsException();
                }

                foreach (Ent.News news in newsLst)
                {
                    // change status to Publish
                    news.Status = NewsStatus.Published;

                    //Serialize the updated news an replicate to anoter region trough Service Bus
                    _serviceBusPublisher.SendMessage(JsonConvert.SerializeObject(news), ReplicationUtils.EntityType.News, ReplicationUtils.Operation.Update);

                    // update news
                    this._newsRepo.UpdateNews(news);
                }
            }
            catch (Exception ex)
            {
                throw;
            }
        }

        /// <summary>
        /// Perform a batch of operations on the resource.
        /// </summary>
        /// <param name="patchData"> Batch of operations. </param>
        public void PatchNews(NewsPatch patchData)
        {
            if (patchData == null)
            {
                throw new NewsPatchDataNullException();
            }

            var deleteitems = patchData.DeleteItems != null ? string.Join(",", patchData.DeleteItems.ToList().ConvertAll<string>(x => x.ToString())) : null;
            var publishnewsitems = patchData.PublishNewsItems != null ? string.Join(",", patchData.PublishNewsItems.ToList().ConvertAll<string>(x => x.ToString())) : null;

            try
            {
                // perform deletes
                if (patchData.DeleteItems != null && patchData.DeleteItems.Any())
                {
                    this.DeleteMultipleNews(patchData.DeleteItems);
                }

                // perform publish
                if (patchData.PublishNewsItems != null && patchData.PublishNewsItems.Any())
                {
                    this.PublishMultipleNews(patchData.PublishNewsItems);
                }
            }
            catch (Exception ex)
            {
                throw;
            }
        }
    }
}
